/*
 * Created on Mar 3, 2010
 */
package com.mgmaps.tools;

import java.util.*;
import javax.microedition.lcdui.Font;
import com.twmacinta.util.MD5;

public class StringTools {

  // for base64 encoding
  public static final char[] BASE64CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray();
  // for urlencoding
  public static final char[] hexArray = "0123456789ABCDEF".toCharArray();
  // random generator
  private static final Random rnd = new Random();
  
  // coord formats
  public static final int COORDFORMAT_D = 0;
  public static final int COORDFORMAT_DM = 1;
  public static final int COORDFORMAT_DMS = 2;
  private static final char[] COORD_CHAR = { 'N', 'E', 'S', 'W' }; 
  
  // computeAgo
  private static final long[] agoTime = { 60, 3600, 604800, 2592000, 31536000, Long.MAX_VALUE };
  private static final String[] agoString = { "minute", "hour", "day", "week", "month", "year" };

  private StringTools() {
  }
  
  
  /**
   * Split a string.
   * 
   * @param s string to split
   * @param c character used for splitting
   * @return an array with the tokens
   */  
  public static String[] split(String s, char c) {
    return split(s, c, false, 0);
  }
  

  /**
   * Split a string.
   * 
   * @param s string to split
   * @param c character used for splitting
   * @param max max number of fields to be returned; 0 for unlimited, non-zero to return all
   *   the remaining string in the last split
   * @param dblquotes whether to ignore delimiters between double quotes
   * @return an array with the tokens
   */
  public static String[] split(String s, char c, boolean dblquotes, int max) {
    int j = 0;
    Vector vector = new Vector();
    
    // add first max-1 components
    int num = 0;
    int i = 0;
    String ss = null;
    int k1;
    int k2;
    for (i=0; num != max-1; i=j+1) {
      k1 = -1;
      k2 = -1;
      j = s.indexOf(c, i);
      if (dblquotes) {
        // should have k1=0
        k1 = s.indexOf('"', i);
        // quote found and before delimiter
        if (k1 >= 0 && k1 < j) {
          // next quote
          k2 = s.indexOf('"', k1+1);
          if (k2 >= 0)
            // recompute next delimiter - should have j=k2+1
            j = s.indexOf(c, k2+1);
        }
      }
      if (j >= 0) {
        if (dblquotes && k1 >= 0 && k2 >= 0)
          ss = s.substring(k1+1, k2);
        else
          ss = s.substring(i, j);
        vector.addElement(ss);
        num++;
      }
      else {
        if (dblquotes && k1 >= 0 && k2 >= 0)
          ss = s.substring(k1+1, k2);
        else
          ss = s.substring(i);
        vector.addElement(ss);
        num++;
        break;
      }
    }
    
    // add the max-th component
    k1 = -1;
    k2 = -1;
    if (max != 0 && j >= 0) {
      if (dblquotes) {
        k1 = s.indexOf('"', i);
        // quote found and before delimiter
        if (k1 >= 0)
          // next quote
          k2 = s.indexOf('"', k1+1);
      }
      if (dblquotes && k1 >= 0 && k2 >= 0)
        ss = s.substring(k1+1, k2);
      else
        ss = s.substring(i);
      vector.addElement(ss);
      num++;
    }

    // convert to array
    String as[] = new String[num];
    vector.copyInto(as);
    
    // return the array
    return as;
  }
  
  
  /**
   * Remove HTML or XML tags in a string (simple version).
   * 
   * @param s the string to parse
   * @return the string with tags removed
   */
  public static String removeTags(String s) {
    char[] c = s.toCharArray();
    StringBuffer result = new StringBuffer();
    boolean inTag = false;
    
    for (int i=0; i<c.length; i++)
      if (c[i] == '<')
        inTag = true;
      else if (c[i] == '>')
        inTag = false;
      else if (!inTag)
        result.append(c[i]);
      
    return result.toString();
  }
  
  
  /**
   * Remove HTML or XML tags in a string, except for &lt;line&gt; which 
   * gets replaced by new line.
   * 
   * @param s the string to parse
   * @return the string with tags removed and newline replaced
   */
  public static String replaceTagsNL(String s, boolean utftags, boolean xtags) {
    if (utftags) {
      s = substituteAll(s, "\\u003c", "<");
      s = substituteAll(s, "\\u003e", ">");
      s = substituteAll(s, "\\u0026", "&");
    }
    else if (xtags) {
      s = substituteAll(s, "\\x3c", "<");
      s = substituteAll(s, "\\x3e", ">");
      s = substituteAll(s, "\\x26", "&");
    }
    return removeTags(substituteAll(s, "<br/>", "\n")).trim();
  }
  
  
  /**
   * A simple search/replace function.
   * 
   * @param s string to modify
   * @param oldSubString old substring
   * @param newSubString replacement substring
   * @return the modified string
   */
  public static String substituteAll(String s, String oldSubString, String newSubString) {
    try {
      String result = "";
      int len = newSubString.length ();
      int oldlen = oldSubString.length ();
      // fall back to String.replace
      if (len == 1 && oldlen == 1)
        return s.replace(oldSubString.charAt(0), newSubString.charAt(0));
      int index = -len;
      while ((index = s.indexOf (oldSubString, index+len)) >= 0) {
        result = s.substring (0, index);
        result += newSubString;
        result += s.substring (index + oldlen);
        s = result;
      }
      return s;
    }
    catch (Exception e) {
      e.printStackTrace ();
      return null;
    }
  }
  
  
  /**
   * Remove all occurences of a char in a string.
   * 
   * @param s the string to parse
   * @param c the char to remove
   * @return the resulting string
   */
  public static String removeChar(String s, char c) {
    char[] cc = s.toCharArray();
    StringBuffer result = new StringBuffer();
    
    for (int i=0; i<cc.length; i++)
      if (cc[i] != c)
        result.append(cc[i]);
      
    return result.toString();
  }
  
  
  /**
   * Encode a block of three characters using base64.
   * @param in array of three characters to encode
   * @param out array of four characters to hold the result
   * @param len the number of characters used
   */
  private static void encodeBase64Block(char[] in, char[] out, int len) {
    out[0] = BASE64CHARS[in[0] >> 2];
    out[1] = BASE64CHARS[((in[0] & 0x03) << 4) | ((in[1] & 0xF0) >> 4)];
    out[2] = (len > 1 ? BASE64CHARS[((in[1] & 0x0F) << 2) | ((in[2] & 0xC0) >> 6)] : '=');
    out[3] = (len > 2 ? BASE64CHARS[in[2] & 0x3F] : '=');
  }
  
  
  /**
   * Encode a string using the base64 encoding.
   * 
   * @param s string to encode
   * @return the encoded string
   */
  public static String encodeBase64(String s) {
    // convert to character array
    char[] chars = s.toCharArray();
    int len = chars.length;
    char[] in = new char[3];
    char[] out = new char[4];
    
    StringBuffer result = new StringBuffer();
    int i;
    
    for (i=0; i<len-3; i+=3) {
      System.arraycopy(chars, i, in, 0, 3);
      encodeBase64Block(in, out, 3);
      result.append(out);
    }
    
    // last segment
    in = new char[3];
    System.arraycopy(chars, i, in, 0, len-i);
    encodeBase64Block(in, out, len-i);
    result.append(out);
    
    return result.toString();
  }
  

  /**
   * Wrapper for MD5.hashString.
   * 
   * @param s string to hash
   * @return md5 hash
   */
  public static String encodeMD5(String s) {
    return MD5.hashString(s);
  }
  
  
  /**
   * Encode a username/password combination to be used with HTTP basic authentication.
   * 
   * @param user username
   * @param pass password
   * @return the encoded string
   */
  public static String encodeUserPass(String user, String pass) {
    if (user.length() == 0 && pass.length() == 0)
      return null;
    return encodeBase64(user + ':' + pass);
  }
  
  
  /**
   * Format a floating point number, round to specific number of decimals.
   * @param number number to format
   * @param decimals number of decimals after the point
   * @return the formatted number
   */
  public static String floatFormat(double number, int decimals) {
    // compute the divisor
    long pow10 = 1;
    for (int i=0; i<decimals; i++)
      pow10 *= 10;
    return floatFormat((long) (MiscTools.round(number)*pow10), decimals);
  }
  
  
  /**
   * Format a floating point number represented as integer.
   * @param number number to format
   * @param decimals number of decimals after the point
   * @return the formatted number
   */
  public static String floatFormat(long number, int decimals) {
    // compute the divisor
    int pow10 = 1;
    for (int i=0; i<decimals; i++)
      pow10 *= 10;
    long trunc = Math.abs(number/pow10);
    long frac = Math.abs(number%pow10);
    
    // build the result
    StringBuffer result = new StringBuffer();
    if (number < 0)
      result.append('-');
    result.append(trunc);
    if (decimals > 0) {
      result.append('.');
      // tail removing
      int i;
      for (i=1; (frac % 10 == 0) && (i<decimals); i++)
        frac /= 10;
      // head padding
      result.append(padZero(Long.toString(frac), decimals-i+1));
    }
    return result.toString();
  }

  
  /**
   * Format coordinate.
   * @param number coordinate
   * @param isLat whether it's a latitude or not
   * @param format dd.dddddd, ddmm.mmm, or ddmmss.ss
   * @return the formatted coord
   */
  public static String formatCoord(double number, boolean isLat, int format) {
    double ax = number;
    int d, m;
    StringBuffer result = new StringBuffer();
    switch(format) {
      case COORDFORMAT_DMS:
        d = (int) Math.floor(ax);
        result.append(d);
        result.append('°');
        ax -= d;
        ax *= 60;
        m = (int) Math.floor(ax);
        result.append(m);
        result.append('\'');
        ax -= m;
        ax *= 60;
        result.append(floatFormat(ax, 1));
        result.append('"');
        break;
        
      case COORDFORMAT_DM:
        d = (int) Math.floor(ax);
        result.append(d);
        result.append('°');
        ax -= d;
        ax *= 60;
        result.append(floatFormat(ax, 3));
        result.append('\'');
        break;
        
      case COORDFORMAT_D:
        result.append(floatFormat(ax, 6));
        break;
    }
    
    result.append(COORD_CHAR[((number >= 0) ? 0 : 2) + (isLat ? 0 : 1)]);
    return result.toString();
  }
  

  /**
   * Format a time (milliseconds) to a string.
   * @param time time in milliseconds
   * @return the time as a string
   */
  public static String timeFormat(long time) {
    int millis = (int) (time % 86400000L);
    StringBuffer result = new StringBuffer();
    result.append(padZero(Integer.toString(millis / 3600000), 2));
    result.append(':');
    millis %= 3600000;
    result.append(padZero(Integer.toString(millis / 60000), 2));
    result.append(':');
    millis %= 60000;
    result.append(padZero(Integer.toString(millis / 1000), 2));
    result.append('.');
    millis %= 1000;
    result.append(padZero(Integer.toString(millis), 3));
    return result.toString();
  }
  
  
  /**
   * Parse a coordinate; ignore the coord format.
   * @param s coordinate to parsed
   * @return the parsed coord
   */
  public static int parseCoord(String s) throws NumberFormatException {
    // split by space
    if (s.length() == 0)
      throw new NumberFormatException("Empty input");
    
    String[] splits = split(s.trim(), ' ');
    
    boolean neg = false;
    if (splits[0].charAt(0) == '-') {
      splits[0] = splits[0].substring(1);
      neg = true;
    }
    
    long result = parseFloat(splits[0], 6);
    if (splits.length > 1)
      result += parseFloat(splits[1], 6) / 60;
    if (splits.length > 2)
      result += parseFloat(splits[2], 6) / 3600;
    return (int) result*(neg?(-1):1);
  }
  
  
  /**
   * Parse a coordinate in GPS format (dddmm.mmmmm).
   * 
   * @param pos the position
   * @param point N, S, E or W
   * @return the coordinate
   */
  public static double parseCoordGPS(String pos, String point) throws NumberFormatException {
    try {
      boolean foundColon = false;
      int dotPos = pos.indexOf('.');
      if (dotPos < 0) // not found, set dotPos to the end of the string
        dotPos = pos.length();
      
      // degrees
      String s = pos.substring(0, dotPos-2);
      if (s.endsWith(":")) {
        s = s.substring(0, s.length()-1);
        foundColon = true;
      }
      double deg = Integer.parseInt(s);
      // minutes
      double min = Integer.parseInt(pos.substring(dotPos-2, dotPos));

      // minute fractions
      s = "0" + pos.substring(dotPos);
      double minfrac = Double.parseDouble(s);
    
      // get coordinate
      // int coord = (int) (deg*1000000L + min*100000L/6 + minfrac*100000L/6/len10);
      double coord = deg + min/60 + minfrac/60;
    
      // negate the coordinate if necessary
      char pointC = point.charAt(0);
      if (!foundColon && (pointC == 'S' || pointC == 'W'))
        coord = -coord;
      
      return coord;
    }
    catch (Exception ex) {
      // an exception occurs if the string is empty or not well-formed
      throw new NumberFormatException(ex.getMessage());
    }
  }
  
  
  public static long parseTimeGPS(String date, String time) throws NumberFormatException {
    try {
      int Y = (date.length() > 6 ? Integer.parseInt(date.substring(4, 8)) : (2000+Integer.parseInt(date.substring(4, 6))));
      int M = Integer.parseInt(date.substring(2, 4));
      int D = Integer.parseInt(date.substring(0, 2));
      int h = Integer.parseInt(time.substring(0, 2));
      int m = Integer.parseInt(time.substring(2, 4));
      int s = Integer.parseInt(time.substring(4, 6));
      int ms = 0;
      if (time.indexOf('.') == 6)
        ms = StringTools.parseFloat(time.substring(6), 3);
      
      Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
      cal.set(Calendar.YEAR, Y);
      cal.set(Calendar.MONTH, M-1);
      cal.set(Calendar.DAY_OF_MONTH, D);
      cal.set(Calendar.HOUR_OF_DAY, h);
      cal.set(Calendar.MINUTE, m);
      cal.set(Calendar.SECOND, s);
      cal.set(Calendar.MILLISECOND, ms);
      
      return cal.getTime().getTime();
    }
    catch (Exception ex) {
      // an exception occurs if the string is empty or not well-formed
      throw new NumberFormatException(ex.getMessage());
    }
  }
  
  
  /**
   * Parse a float number into an integer.
   * 
   * @param s float to parse
   * @param decimals number of decimals to consider
   * @return the integer number
   */
  public static int parseFloat(String s, int decimals) {
    // compute the multiplier
    int pow10 = 1;
    for (int i=0; i<decimals; i++)
      pow10 *= 10;
    
    // find the dot
    int dotPos = s.indexOf('.');
    if (dotPos < 0)
      return Integer.parseInt(s) * pow10;
    
    // build the result
    int number = ((dotPos == 0) ? 0 : Integer.parseInt(s.substring(0, dotPos)) * pow10);
    
    // keep exactly 'decimals' digits
    String pze = padZeroEnd(s.substring(dotPos+1), decimals);
    if (pze.length() > decimals)
      pze = pze.substring(0, decimals);
    
    int frac = Integer.parseInt(pze);
    if (s.charAt(0) == '-')
      number -= frac;
    else
      number += frac;
    
    return number;
    
    // return (int) henson.midp.Float10.parse(s, 10).Mul(pow10).toLong();
  }
  
  
  /**
   * Pad a string with leading zeroes.
   * 
   * @param s the string to pad
   * @param chars minimum number of characters
   * @return the padded string
   */
  public static String padZero(String s, int chars) {
    StringBuffer result = new StringBuffer();
    int len = s.length();
    for (int i=len; i<chars; i++)
      result.append('0');
    result.append(s);
    return result.toString();
  }
  
  
  /**
   * Pad a string with trailing zeroes.
   * 
   * @param s the string to pad
   * @param chars minimum number of characters
   * @return the padded string
   */
  public static String padZeroEnd(String s, int chars) {
    StringBuffer result = new StringBuffer();
    int len = s.length();
    result.append(s);
    for (int i=len; i<chars; i++)
      result.append('0');
    return result.toString();
  }
  
  
  /**
   * Replace \\uxxxx and \xxx with UTF-16 code.
   * @param s string to parse
   * @return the replaced string
   */
  public static String replaceUnicodes(String s) {
    char[] c = s.toCharArray();
    StringBuffer result = new StringBuffer();
    
    for (int i=0; i<c.length; i++)
      if (c[i] == '\\') {
        if (i < c.length-5 && c[i+1] == 'u') {
          // compute the resulting character
          char res = (char) 0;
          for (int k=1; k<=4; k++) {
            res <<= 4;
            res += hexCharCode(c[i+k+1]);
          }
          
          result.append(res);
          i += 5;
        }
        else if (i < c.length-3 && c[i+1] == 'x') {
          // compute the resulting character
          char res = (char) 0;
          for (int k=1; k<=2; k++) {
            res <<= 4;
            res += hexCharCode(c[i+k+1]);
          }
          
          result.append(res);
          i += 3;
        }
        // not enough characters left?
        else
          result.append(c[i]);
      }
      else
        result.append(c[i]);
    
    return result.toString();
  }
  
  
  /**
   * Generate a MIME boundary.
   * 
   * @return MIME boundary string, 28 chars
   */
  public static String generateMimeBoundary() {
    char[] result = new char[28];
    for (int i=0; i<12; i++)
      result[i] = '-';
    for (int i=12; i<28; i++)
      result[i] = BASE64CHARS[rnd.nextInt(64)];
    return new String(result);
  }
  
  
  /**
   * Convert a string to byte array, parse as UTF-8
   * @param s string to parse
   * @return the byte array
   */
  public static byte[] utfStringToByteArray(String s) {
    byte[] b = null;
    
    try {
      b = s.getBytes("utf-8");
    }
    catch (Exception ex) {
      try {
        b = s.getBytes("UTF8");
      }
      catch (Exception ex2) {
        b = s.getBytes();
      }
    }
    
    return b;
  }

  
  /**
   * URLEncode method.
   * 
   * @param s string to urlencode
   * @return urlencoded string
   */
  public static String urlEncode(String s) {
    byte[] b = utfStringToByteArray(s);
    
    StringBuffer result = new StringBuffer();
    
    for (int i=0; i<b.length; i++)
      if ((b[i] >= 'A' && b[i] <= 'Z') || (b[i] >= 'a' && b[i] <= 'z') || 
          (b[i] >= '0' && b[i] <= '9'))
        result.append((char) b[i]);
      else {
        result.append('%');
        result.append(hexArray[((b[i]+256) >> 4) & 0x0F]);
        result.append(hexArray[b[i] & 0x0F]);
      }
    
    return result.toString();
  }
  
  
  /**
   * Get the number (code) for a hex char. 
   * @param c char to process
   * @return the code
   */
  public static int hexCharCode(char c) {
    if (c >= '0' && c <= '9')
      return c-'0';
    else if (c >= 'A' && c <= 'F')
      return c-'A'+10;
    else if (c >= 'a' && c <= 'f')
      return c-'a'+10;
    else // error
      return 0;    
  }
  
  
  /**
   * URLDecode method.
   * @param s the string to decode
   * @return the decoded string
   */
  public static String urlDecode(String s, boolean plus) {
    char[] c = s.toCharArray();
    StringBuffer result = new StringBuffer();
    
    for (int i=0; i<c.length; i++)
      if (c[i] == '%') {
        // not enough characters left?
        if (i >= c.length-2)
          break;
        
        // compute the resulting character
        char res = (char) 0;
        for (int k=1; k<=2; k++) {
          res <<= 4;
          res += hexCharCode(c[i+k]);
        }
        
        result.append(res);
        i += 2;
      }
      else if (plus && c[i] == '+')
        result.append(' ');
      else
        result.append(c[i]);
    
    return result.toString();
  }
  

  /**
   * URL decode entities (html_entity_decode)
   * @param s string with encoded entities
   * @return decoded string
   */
  public static String urlDecodeEntities(String s) {
    String[] repl1 = { "amp;quot;", "amp;apos;", "amp;", "lt;", "gt;", "quot;", "apos;", "circ;", "tilde;", "nbsp;", "hellip;" };
    char[] repl2 = { '"', '\'', '&', '<', '>', '"', '\'', '^', '~', ' ', '�' };

    char[] c = s.toCharArray();
    StringBuffer result = new StringBuffer();
    
    for (int i=0; i<c.length; i++)
      if (c[i] == '&') {
        // not enough characters left?
        if (i >= c.length-3)
          break;
        
        String sub = s.substring(i+1); 
        // &#
        if (c[i+1] == '#') {
          // search for ;
          int pos = sub.indexOf(';');
          if (pos < 0)
            // not well formed
            break;
          
          String sub2 = sub.substring(1, pos);
          
          // compute the resulting character
          try {
            char res = (char) Integer.parseInt(sub2);
            result.append(res);
          }
          catch (NumberFormatException nfe) {
            break;
          }
          
          i += pos+1;
          continue;
        }
        
        // check known special entities
        boolean continuing = false;
        for (int j=0; j<repl1.length; j++)
          if (sub.startsWith(repl1[j])) {
            result.append(repl2[j]);
            i += repl1[j].length();
            continuing = true;
            break;
          }

        // unknown special entity, just append it
        if (!continuing)
          result.append(c[i]);
      }
      else // no entity, append it
        result.append(c[i]);
    
    return result.toString();
  }
  
  
  /**
   * Convert a byte array to a string, with UTF-8 as encoding.
   * 
   * @param data byte array to convert
   * @return the resulting string
   */
  public static String byteArrayToString(byte[] data) {
    String sdata = null;
    try {
      sdata = new String(data, "utf-8");
    }
    catch (Exception ex) {
      sdata = new String(data);
    }
    return sdata;
  }
  
  
  /**
   * Convert a string to a byte array, keep only low 8 bits.
   * @param s string to convert
   * @return the byte array
   */
  public static byte[] stringToByteArray(String s) {
    char[] c = s.toCharArray();
    byte[] b = new byte[c.length];
    for (int i=0; i<c.length; i++)
      b[i] = (byte) c[i];
    return b;
  }
  
  
  /**
   * Convert a vector to a string array.
   * @param v vector to convert
   * @return the string array
   */
  public static String[] toStringArray(Vector v) {
    String[] res = new String[v.size()];
    v.copyInto(res);
    return res;
  }
  
  
  /**
   * Extract JS content.
   * 
   * @param s string to process (JS/JSON)
   * @param header the heading text
   * @param escape whether to escape ' as \\042
   * @return the extracted content
   * @throws Exception if an exception occurs
   */
  public static String extractJSONContent(String s, String header, String escape) throws Exception {
    String ss = header + escape;
    int pos = s.indexOf(ss);
    if (pos < 0)
      throw new Exception();
    String tmpString = s.substring(pos+ss.length());
    
    pos = tmpString.indexOf(escape);
    if (pos < 0)
      throw new Exception();
    return tmpString.substring(0, pos);
  }

  
  /**
   * Extract XML Content, decode just entities.
   * 
   * @param s string to process (XML)
   * @param tag the tag
   * @return the extracted content
   * @throws Exception if an exception occurs
   */
  public static String extractXMLContent(String s, String tag) throws Exception {
    return extractXMLContent(s, tag, false, false);
  }

  
  /**
   * Extract XML Content for all children.
   * 
   * @param s string to process (XML)
   * @param tag the child tag
   * @return the extracted content
   * @throws Exception if an exception occurs
   */
  public static String[] extractAllXMLChildren(String s, String tag) throws Exception {
    return extractAllXMLChildren(s, tag, false, false);
  }

  
  /**
   * Extract XML Content for all children.
   * 
   * @param s string to process (XML)
   * @param tag the child tag
   * @return the extracted content
   * @throws Exception if an exception occurs
   */
  public static String[] extractAllXMLChildren(String s, String tag, boolean utftags, boolean xtags) throws Exception {
    String ss = s;
    Vector v = new Vector();
    
    while(true) {
      // title
      String open = utftags ? "\\u003c" : xtags ? "\\x3c" : "<";
      String close = utftags ? "\\u003e" : xtags ? "\\x3e" : ">";
      int pos = ss.indexOf(open + tag);
      if (pos < 0) // not found? break
        break;
      ss = ss.substring(pos+open.length()+tag.length());    
    
      pos = ss.indexOf(close);
      if (pos < 0) // not found? break
        throw new Exception();
      ss = ss.substring(pos+close.length());        
      
      pos = ss.indexOf(open + "/" + tag + close);
      if (pos < 0) // not found? break
        throw new Exception();
      
      v.addElement(urlDecodeEntities(ss.substring(0, pos)));
      ss = ss.substring(pos+open.length()+1+tag.length()+close.length());
    }
    
    final String[] result = new String[v.size()];
    v.copyInto(result);
    v.removeAllElements();
    return result;
  }

  
  /**
   * Extract XML Content, decode just entities.
   * 
   * @param s string to process (XML)
   * @param tag the tag
   * @return the extracted content
   * @throws Exception if an exception occurs
   */
  public static String extractXMLContent(String s, String tag, boolean utftags, boolean xtags) throws Exception {
    /*
    // tag without args
    String shortTag = null;
    int pos = tag.indexOf(" ");
    if (pos < 0)
      shortTag = tag;
    else
      shortTag = tag.substring(0, pos);
    */
    
    // title
    String open = utftags ? "\\u003c" : xtags ? "\\x3c" : "<";
    String close = utftags ? "\\u003e" : xtags ? "\\x3e" : ">";
    int pos = s.indexOf(open + tag);
    if (pos < 0) // not found? break
      throw new Exception();
    String remainingString = s.substring(pos+open.length()+tag.length());    
  
    pos = remainingString.indexOf(close);
    if (pos < 0) // not found? break
      throw new Exception();
    remainingString = remainingString.substring(pos+close.length());        
  
    pos = remainingString.indexOf(open + "/" + tag + close);
    if (pos < 0) // not found? break
      throw new Exception();
    
    /*
    // find another starting tag
    int pos2 = remainingString.indexOf("<" + shortTag);
    if (pos2 >= 0 && pos2 < pos)
      pos = pos2;
    */

    return urlDecodeEntities(remainingString.substring(0, pos));    
  }
  
  
  /**
   * Remove CDATA from a URL.
   * @param s string with CDATA
   * @return string without CDATA
   */
  public static String removeCDATA(String s) {
    s = s.trim();
    int pos = s.indexOf("<![CDATA[");
    
    if (pos < 0)
      pos = -9;
    s = s.substring(pos+9).trim();
    if (s.endsWith("]]>"))
      s = s.substring(0, s.length()-3).trim();
    
    return s;
  }
  

  /**
   * Extract the domain from a URL. Port included in the domain specification is
   *   not processed.
   * @param url url to process
   * @return domain
   */
  public static String extractDomain(String url) {
    int p = url.indexOf('/', 7); // strlen("http://")
    return url.substring(7, p).toLowerCase();
  }
  
  
  /**
   * Word-wrap text.
   * 
   * @param text the text to wrap
   * @param font the font used
   * @param fwidth text field width 
   * @param currentX start of first line
   * @return a string array of lines containing the wrapped text
   */
  public static String[] wrapText(String text, Font font, int fwidth, int currentX) {
    // wrapped text lines
    Vector wt = new Vector();
    
    int len = text.length(); 
    int first = 0;
    int last = 0;
    boolean emptylineflag = false;
    
    for (int i=0; i<len; i++) {
      // split at '\n'
      if (text.charAt(i) == '\n') {
        wt.addElement(text.substring(first, i));
        first = i+1;
        last = i;
        emptylineflag = false;
        continue;
      }
      
      // update last if necessary
      if (text.charAt(i) == ' ') {
        last = i;
        // don't check to split - we would have splitted at the previous char
        continue;
      }
      
      // check length, see if we should split
      int w = font.substringWidth(text, first, i-first+1);
      // we should not split if we printed nothing on the previous line 
      if ((first == 0 && w+currentX > fwidth) ||
          (first != 0 && w > fwidth)) {
        // yep, we should split, first to last, then the rest
        // if last is equal to first, the first word doesn't fit, so add an empty string
        // but set a flag to avoid adding it again
        if (first < last || (first == last && !emptylineflag)) {
          String s = text.substring(first, last);
          wt.addElement(s);
        }
        if (first < last) {
          // start with last+1
          emptylineflag = false;
          first = last+1;
        }
        else
          emptylineflag = true;
      }
    }
    
    // add the last string
    wt.addElement(text.substring(first));
    
    return toStringArray(wt);    
  }


  /**
   * Compute the maximum length of an array of strings.
   * @param wrappedText wrapped string
   * @return maximum length
   */
  public static int maxLength(String[] wrappedText, Font font) {
    int max = 0;
    for (int i=0; i<wrappedText.length; i++) {
      final int width = font.stringWidth(wrappedText[i]);
      if (width > max)
        max = width;
    }
    return max;
  }

  
  /**
   * Sort a set of strings in array1, update array2 accordingly
   * @param array1 sort keys
   * @param array2 associated data
   */
  public static void shellsort(String[] array1, String[] array2) {
    for (int inc = array1.length>>1; inc>0; inc = (inc == 2 ? 1 : (inc*10/22)))
      for (int i=inc; i<array1.length; i++) {
        int j = i;
        String t1 = array1[i];
        String t2 = array2[i];
        while (j>=inc && array1[j-inc].compareTo(t1)>0) {
          array1[j] = array1[j-inc];
          array2[j] = array2[j-inc];
          j -= inc;
        }
        array1[j] = t1;
        array2[j] = t2;
      }
  }

  
  /**
   * ParseInt wrapper.
   * @param s string to parse
   * @return integer value
   */
  public static int parseInt(final String s) {
    final String s2 = s.trim();
    final int s2len = s2.length();
    int i;
    for (i=0; i < s2len && s2.charAt(i) == '0'; )
      i++;
    if (i == s2len)
      return 0;
    else
      return Integer.parseInt(s2.substring(i));
  }
  
  
  /**
   * Process a string and remove all non-phone chars.
   * 
   * @param s the string processed
   * @return the phone number
   */
  public static String parseTel(String s) {
    // convert to char array
    char[] chars = s.trim().toCharArray();
    if (chars.length == 0)
      return "";
    
    // allow 0-9, +, *, #, ignore anything else
    StringBuffer result = new StringBuffer();
    for (int i=0; i<chars.length; i++) {
      char c = chars[i];
      if ((c >= '0' && c <= '9') || c == '+' || c == '*' || c == '#')
        result.append(c);
    }
    return result.toString();
  }

  
  /**
   * Build an "ago" string for a date object compared to current datetime.
   * @param dt date time to compare
   * @return "x days/minutes/... ago"
   */
  public static String computeAgo(Date dt) {
    if (dt == null)
      return "Unknown";
    long dif = (System.currentTimeMillis()-dt.getTime())/1000;
    if (dif < 0)
      return dt.toString();
    StringBuffer result = new StringBuffer();
    if (dif < 60)
      return "now";
    int len = agoTime.length;
    for (int i=1; i<len; i++)
      if (dif < agoTime[i]) {
        final int val = (int) (dif/agoTime[i-1]);
        result.append(val);
        result.append(" ");
        result.append(agoString[i-1]);
        result.append(val == 1 ? "" : "s");
        result.append(" ago");
        return result.toString();
      }
  
    return dt.toString();
  }
  
  /**
   * Format a Date object.
   * @param d date to format
   * @return date in format "yyyy-mm-dd hh:mm:ss"
   */
  public static String dateTimeFormat(Date d) {
    return dateTimeFormat(d, " ");
  }
  
  /**
   * Format a Date object.
   * @param d date to format
   * @return date in format "yyyy-mm-dd hh:mm:ss"
   */
  public static String dateTimeFormat(Date d, String sep) {
    final Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
    c.setTime(d);
    final String year = padZero(Integer.toString(c.get(Calendar.YEAR)), 4);
    final String month = padZero(Integer.toString(c.get(Calendar.MONTH)+1), 2);
    final String day = padZero(Integer.toString(c.get(Calendar.DAY_OF_MONTH)), 2);
    final String hour = padZero(Integer.toString(c.get(Calendar.HOUR_OF_DAY)), 2);
    final String minute = padZero(Integer.toString(c.get(Calendar.MINUTE)), 2);
    final String second = padZero(Integer.toString(c.get(Calendar.SECOND)), 2);
    return year + '-' + month + '-' + day + sep + hour + ':' + minute + ':' + second;
  }
}
